function KeyringSynchronizer()
{
    var cached_authToken = null;
    var cached_sheetId = null;

    /* eval XPath expression against a DOM node, returning an array */
    function evaluateXPath(aNode, aExpr)
    {
        var xpe = aNode.ownerDocument || aNode; // = new XPathEvaluator();
        var nsResolver = xpe.createNSResolver(aNode.ownerDocument == null ?
            aNode.documentElement : aNode.ownerDocument.documentElement);
//            var nsResolver = xpe.createNSResolver(xpe.documentElement);
        var result = xpe.evaluate(aExpr, aNode, nsResolver, 0, null);
        var found = [];
        var res;
        while (res = result.iterateNext())
            found.push(res);
        return found;
    }

    function getGooglePassword()
    {
        var hostname = 'https://www.google.com';
        var submiturl = 'https://www.google.com';
        var username = lucidapi.config.google_login;
        if (!username)
            return null;
        var loginMgr = lucidapi.getXpcomService("@mozilla.org/login-manager;1","nsILoginManager");
        var logins = loginMgr.findLogins({}, hostname, submiturl, null);
        for (var i = 0; i < logins.length; i++) {
            if (logins[i].username == username) {
                return logins[i].password;
            }
        }
        return null;
    }

    function validateAuthToken(force)
    {
        if (!force && cached_authToken)
            return true;
        var password = getGooglePassword();
        if (!password)
            return false;
        var req = new NSIXMLHttpRequest();
        req.open('POST', "https://www.google.com/accounts/ClientLogin", false);
        req.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
        var sendStr = 'accountType=GOOGLE';
        sendStr += "&Email=" + lucidapi.config.google_login;
        sendStr += "&Passwd=" + password;
        sendStr += "&service=wise";
        sendStr += "&source=Emergent-Lucidity-" + LUCID_VERSION;
        req.send(sendStr);
        if(req.status == 200)
        {
            var rsplines = req.responseText.split("\n");
            for (var ii = 0; ii < rsplines.length; ii++)
            {
                var aline = rsplines[ii];
                var authidx = aline.indexOf("Auth=");
                if (authidx > -1)
                {
                    cached_authToken = aline.substr(authidx + "Auth=".length);
                    return true;
                }
            }
        }
        else
        {
            lucidapi.debug("req.status %d", req.status);
            lucidapi.debug("responseText:");
            lucidapi.debug("%s",req.responseText);
        }
        return false;
    }

    function dumpResponse(req)
    {
        if (req)
        {
            var fileext = ("responseXML" in req) ? "xml" : "txt";
            var tmpfile = new NSILocalFile("C:\\gapiresp." + fileext);
            saveFileData(tmpfile,req.responseText);
            var ios = lucidapi.getXpcomService("@mozilla.org/network/io-service;1","nsIIOService");
            var tmpurl = ios.newFileURI(tmpfile);
            lucidapi.openTab(tmpurl.spec);
        }
    }

    function getFeedRequest(urlstr)
    {
        if (!validateAuthToken())
        {
            return null;
        }
        var req = new NSIXMLHttpRequest();
        req.open('GET', urlstr, false);
        req.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
        req.setRequestHeader("Authorization", 'GoogleLogin auth="' + cached_authToken + '"');
        req.send(null);
        dumpResponse(req);
        return req;
    }

    function getSpreadsheetId()
    {
        if (cached_sheetId)
            return cached_sheetId;

        var req = getFeedRequest("http://spreadsheets.google.com/feeds/spreadsheets/private/full");
        if (!req || req.status != 200)
        {
            lucidapi.debug("Could not get spreadsheets feed!");
            return null;
        }
        var rspdoc = req.responseXML;
        var titleNodes = evaluateXPath(rspdoc,"//*[name()='title' and @type='text']");
        for (var ii = 0; ii < titleNodes.length; ii++)
        {
            var tnode = titleNodes[ii];
            if (tnode.textContent != lucidapi.config.key_src_url)
                continue;
            var idnodes = evaluateXPath(tnode, "preceding-sibling::*[name()='id']");
            var idtxt = idnodes[0].textContent;
            var retval = idtxt.substr("http://spreadsheets.google.com/feeds/spreadsheets/private/full/".length);
            if (retval)
                cached_sheetId = retval;
            return cached_sheetId;
        }
        return null;
    };

    function parseCellFeed()
    {
        var sheetId = getSpreadsheetId();
        if (!sheetId)
            return null;
        var urlstr = "";
        urlstr = "http://spreadsheets.google.com/feeds/cells/"
        urlstr += sheetId;
        urlstr += "/1/private/full?max-col=1&min-row=2&return-empty=true";
        var req = getFeedRequest(urlstr);
        if (!req || req.status != 200)
        {
            lucidapi.debug("Could not get list feed!");
            return null;
        }
        var rspdoc = req.responseXML;
        var cellNodes = evaluateXPath(rspdoc,"//*[name()='gs:cell']");
        var retval = [];
        for (var ii = 0; ii < cellNodes.length; ii++)
        {
            var cnode = cellNodes[ii];
            var celldat = {};
            celldat.row = cnode.getAttribute("row");
            celldat.col = cnode.getAttribute("col");
            celldat.inputValue = cnode.hasAttribute("inputValue") ? cnode.getAttribute("inputValue") : "";
//                sync_importKeyData(cnode.textContent);
            var editlinks = evaluateXPath(cnode, "preceding-sibling::*[name()='link' and @rel='edit']");
            celldat.editlinkHref = editlinks[0].getAttribute("href");
//                var updatedNodes = evaluateXPath(cnode, "preceding-sibling::*[name()='updated']");
//                celldat.updated = updatedNodes[0].textContent;
//                var titleNodes = evaluateXPath(cnode, "preceding-sibling::*[name()='title']");
//                celldat.title = titleNodes[0].textContent;
            retval.push(celldat);
        }
        return retval;
    };

    function pushSingleCell(celldat)
    {
        var urlStr = "http://spreadsheets.google.com/feeds/cells/" + cached_sheetId + "/1/private/full";
        var req = new NSIXMLHttpRequest();
        req.open('PUT', celldat.editlinkHref, false);
        req.setRequestHeader("Content-Type", "application/atom+xml");
        req.setRequestHeader("Authorization", 'GoogleLogin auth="' + cached_authToken + '"');
        var sendStr = '<entry xmlns="http://www.w3.org/2005/Atom" xmlns:gs="http://schemas.google.com/spreadsheets/2006">';
        sendStr += '<id>' + urlStr + '/R' + celldat.row + 'C' + celldat.col + '</id>';
        sendStr += '<link rel="edit" type="application/atom+xml" href="' + celldat.editlinkHref + '"/>';
        sendStr += '<gs:cell row="' + celldat.row + '" col="' + celldat.col + '" inputValue="' + celldat.inputValue + '"/>';
        sendStr += '</entry>';
        req.send(sendStr);
        return req.status == 200;
    }

    function pushCellList(celllist)
    {
        lucidapi.assert(celllist instanceof Array);
        var urlStr = "http://spreadsheets.google.com/feeds/cells/" + cached_sheetId + "/1/private/full";
        var req = new NSIXMLHttpRequest();
        req.open('POST', urlStr + "/batch", false);
        req.setRequestHeader("Content-Type", "application/atom+xml");
        req.setRequestHeader("Authorization", 'GoogleLogin auth="' + cached_authToken + '"');
        var sendStr = '<feed xmlns="http://www.w3.org/2005/Atom"';
        sendStr += ' xmlns:batch="http://schemas.google.com/gdata/batch"';
        sendStr += ' xmlns:gs="http://schemas.google.com/spreadsheets/2006">';
        sendStr += '<id>' + urlStr + '</id>';
        for (var ii = 0; ii < celllist.length; ii++)
        {
            var celldat = celllist[ii];
            if (!celldat)
                continue;
            sendStr += '<entry>';
            sendStr += '<batch:id>/R' + celldat.row + 'C' + celldat.col + '</batch:id>';
            sendStr += '<batch:operation type="update"/>';
            sendStr += '<id>' + urlStr + '/R' + celldat.row + 'C' + celldat.col + '</id>';
            sendStr += '<link rel="edit" type="application/atom+xml" href="' + celldat.editlinkHref + '"/>';
            sendStr += '<gs:cell row="' + celldat.row + '" col="' + celldat.col + '" inputValue="' + celldat.inputValue + '"/>';
            sendStr += '</entry>';
        }
        sendStr += '</feed>';
        req.send(sendStr);
        return req;
    }

    function parseOutMod(val)
    {
        var retval = 0;
        if (val.indexOf("modtime") == 0)
        {
            retval = parseInt(val.substr("modtime".length),10);
        }
        return retval;
    }

    function parseGoogleTime(datestr)
    {
        var d = new Date();
        var tidx = datestr.indexOf("T");
        var pret = datestr.substr(0,tidx).split("-");
        for (var ii = 0; ii < pret.length; ii++)
        {
          pret[ii] = parseInt(pret[ii],10);
        }
        d.setUTCFullYear(pret[0],pret[1] - 1,pret[2]);
        pret = datestr.substr(tidx+1).split(":");
        pret[2] = pret[2].substr(0,pret[2].indexOf("."));
        for (ii = 0; ii < pret.length; ii++)
        {
          pret[ii] = parseInt(pret[ii],10);
        }
        d.setUTCHours(pret[0],pret[1],pret[2]);
        return d;
    }

    function unwrapKey(data)
    {
        if (data && data.indexOf("keydat ") == 0)
        {
            try
            {
                var thedat = r2s(data.substr("keydat ".length));
                var tmpkey = pgp_loadKeyChain(thedat);
                if (tmpkey && tmpkey.length > 0)
                {
                    return tmpkey[0];
                }
            }
            catch (e)
            {
                Components.utils.reportError(e);
            }
        }
        return null;
    }

    this.pushData = function(localmap)
    {
        var retval = false;
        var celldata = parseCellFeed();
        var savelist = [];
        var emptylist = [];
        var pushlist = [];
        if (celldata)
        {
            var remoteDat = celldata.shift();
            while (remoteDat)
            {
                var remoteKey = unwrapKey(remoteDat.inputValue);
                if (remoteKey)
                {
                    var keyid = remoteKey.getKeyId();
                    if (localmap[keyid])
                    {
                        lucidapi.debug("found matching keys: %s", keyid);
                        savelist.push(localmap[keyid]); // keep local when match for now
                        delete localmap[keyid];
                    }
                    else
                    {
                        lucidapi.debug("adding to local ring: %s", keyid);
                        savelist.push(remoteKey);
                    }
                }
                else
                {
                    emptylist.push(remoteDat);
                }
                remoteDat = celldata.shift();
            }
            for (keyid in localmap)
            {
                var localDat = localmap[keyid];
                savelist.push(localDat);
                remoteDat = emptylist.shift();
                if (remoteDat)
                {
                    remoteDat.inputValue = "keydat " + s2r(localDat.serialize());
                    lucidapi.debug("adding to remote ring: %s", keyid);
                    pushlist.push(remoteDat);
                }
                else
                {
                    // whoops out of empties
                }
            }

            if (pushlist.length > 0)
            {
                var req = pushCellList(pushlist);
                retval = req.status == 200;
                dumpResponse(req);
            }
            else
            {
                retval = true;
            }
        }
        lucidapi.debug("final retval: %s", retval);
        return savelist;
    }

    function sync_importKeyData(unilist)
    {
        var secser = "";
        var pubser = "";
        for (var ii = 0; ii < unilist.length; ii++)
        {
            var key = unilist[ii];
            if (key.isSecretKey())
            {
                secser += key.serialize();
                pubser += key.serialize(true);
            }
            else
            {
                pubser += key.serialize();
            }
        }

        lucid_saveKeyData(pubser,secser);
    }

    this.doSync = function()
    {
        // map of unprocessed locals and array of to-save-locally in one
        var localmap = [];
        var keyChain = lucidapi.getKeyChain(false);
        for (var ii = 0; ii < keyChain.length; ii++)
        {
            localmap[keyChain[ii].getKeyId()] = keyChain[ii];
        }
        keyChain = lucidapi.getKeyChain(true);
        for (ii = 0; ii < keyChain.length; ii++)
        {
            localmap[keyChain[ii].getKeyId()] = keyChain[ii];
        }
        var savelist = this.pushData(localmap);
        sync_importKeyData(savelist);
    }
}
